package de.argh.jteas.mojo.controller;

import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;

import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlType;

import org.apache.maven.plugin.logging.Log;
import org.apache.maven.plugin.logging.SystemStreamLog;

import de.argh.jteas.mojo.exception.MappingNotFoundException;
import de.argh.jteas.mojo.model.ASClass;
import de.argh.jteas.mojo.model.ASField;
import de.argh.jteas.mojo.model.ASVisibility;
import de.argh.jteas.mojo.model.annotation.ASAnnotationTag;
import de.argh.jteas.mojo.model.annotation.ASClassAnnotation;
import de.argh.jteas.mojo.model.annotation.ASFieldAnnotation;
import de.argh.jteas.mojo.model.settings.Settings;

public class TypeCollector {

	final private String mappingFile = "mapping.properties";

	private Log log;
	private Properties mapping;
	private List<ASClass> asClasses;
	private Settings settings;
	private List<Class<?>> objectFactories;

	/**
	 * Constructor
	 * @param settings
	 * @param objectFactories
	 */
	public TypeCollector(Settings settings, List<Class<?>> objectFactories) {
		this.settings = settings;

		this.asClasses = new ArrayList<ASClass>();
		this.objectFactories = objectFactories;
	}

	/**
	 * 
	 * @param basePackage
	 * @throws MappingNotFoundException
	 */
	public void parseThroughObjectFactories(String basePackage) throws MappingNotFoundException {

		for (Class<?> objFactory : objectFactories) {
			for (Method method : objFactory.getDeclaredMethods()) {
				Class<?> returnType = method.getReturnType();
				if (returnType != null) {
					String packageName = returnType.getPackage().getName();
					if (packageName.startsWith(basePackage)) {
						asClasses.add(parseThroughClazz(returnType));

					}
				}
			}
		}
	}

	/**
	 * 
	 * @param clazz
	 * @return
	 * @throws MappingNotFoundException
	 */
	private ASClass parseThroughClazz(Class<?> clazz) throws MappingNotFoundException {
		ASClass asClazz = new ASClass(clazz.isEnum());
		asClazz.setPackageName(clazz.getPackage().getName());
		asClazz.setName(clazz.getSimpleName());

		for (Annotation anno : clazz.getAnnotations()) {
			if (anno instanceof XmlType) {
				XmlType xType = (XmlType) anno;
				asClazz.setFieldOrder(xType.propOrder());
			}
		}

		Class<?> superClazz = clazz.getSuperclass();
		if (!superClazz.equals(Object.class)) {
			asClazz.setExtender(parseThroughClazz(superClazz));
		} else {
			asClazz.setExtender(null);
		}

		getLog().info(asClazz.getFullName());

		asClazz.setAnnotation(generateClazzAnnotation(asClazz, clazz));

		for (Field field : clazz.getDeclaredFields()) {
			ASField asf = new ASField();

			asf.setName(field.getName());
			asf.setType(analyseFieldType(field));

			boolean req = false;
			for (Annotation anno : field.getAnnotations()) {
				if (anno instanceof XmlElement) {
					XmlElement xElem = (XmlElement) anno;
					req = xElem.required();
				} else if (anno instanceof XmlAttribute) {
					XmlAttribute xAttr = (XmlAttribute) anno;
					req = xAttr.required();
				}
			}
			asf.setRequired(req);

			asf.setNeedImport(fieldNeedImport(asClazz, asf.getType()));

			ASVisibility visibility = ASVisibility.PROTECTED;
			asf.setVisibility(visibility);

			int order = getOrderFromProps(asClazz, asf.getName());
			asf.setAnnotation(analyseFieldAnnotation(order, field));

			asClazz.getFields().add(asf);
		}

		return asClazz;
	}

	/**
	 * 
	 * @param asClazz
	 * @param fieldName
	 * @return
	 */
	private int getOrderFromProps(ASClass asClazz, String fieldName) {
		int number = 0;
		if (asClazz.getExtender() != null) number = asClazz.getExtender().getFields().size();

		for (String prop : asClazz.getFieldOrder()) {
			number++;
			if (prop.equals(fieldName)) { return number; }
		}

		return 0;
	}

	/**
	 * 
	 * @param field
	 * @return
	 * @throws MappingNotFoundException
	 */
	private ASClass analyseFieldType(Field field) throws MappingNotFoundException {
		ASClass rVal = null;

		String packageName = field.getType().getPackage().getName();

		if (packageName.startsWith(settings.getBasePackage())) {
			if (!asClassExists(packageName + "." + field.getName())) {

				if (field.getType().isEnum() && settings.isStringForEnum()) {
					ASClass tmpASClazz = new ASClass();
					tmpASClazz.setName("String");
					rVal = tmpASClazz;

				} else {
					if (!asClassExists(field.getType().getName())) {
						rVal = parseThroughClazz(field.getType());
						asClasses.add(rVal);
					} else {
						rVal = getAsClass(field.getType().getName());
					}
				}

			}
		} else {

			String mappingType = getMapping().getProperty(field.getType().getName());
			if (mappingType != null) {
				rVal = new ASClass(mappingType);

			} else {
				throw new MappingNotFoundException("No mapping found for " + mappingType + "; " + field.getName());

			}
		}
		return rVal;
	}

	/**
	 * 
	 * @param asClazz
	 * @param type
	 * @return
	 */
	private boolean fieldNeedImport(ASClass asClazz, ASClass type) {
		if (type.getPackageName() == null || type.getPackageName().trim() == "") { return false; }
		return !asClazz.getPackageName().equals(type.getPackageName());
	}

	/**
	 * 
	 * @param asClazz
	 * @param clazz
	 * @return
	 */
	private ASClassAnnotation generateClazzAnnotation(ASClass asClazz, Class<?> clazz) {
		ASClassAnnotation rVal = new ASClassAnnotation();
		rVal.setTag(ASAnnotationTag.XMLCLASS);
		rVal.setOrderd(settings.isOrderedFields());

		XmlType xType = clazz.getAnnotation(XmlType.class);
		rVal.setAlias(xType.name());

		rVal.setVersion(settings.getXmlAnnotationVersion());

		// Setting XML-Prefix and URI
		if (settings.getUriPackageProperties() != null) {
			String uriPhrase = settings.getUriPackageProperties().getProperty(asClazz.getPackageName());
			if (uriPhrase != null && !uriPhrase.isEmpty()) {
				rVal.setPrefix(uriPhrase.substring(0, uriPhrase.indexOf(":")));
				rVal.setUri(uriPhrase.substring(uriPhrase.indexOf(":")+1, uriPhrase.length()));
			}
		}
		return rVal;
	}

	/**
	 * 
	 * @param fullName
	 * @return
	 */
	private boolean asClassExists(String fullName) {
		for (ASClass asClass : asClasses) {
			if (asClass.getFullName().equals(fullName)) { return true; }
		}
		return false;
	}

	/**
	 * 
	 * @param fullName
	 * @return
	 */
	private ASClass getAsClass(String fullName) {
		for (ASClass asClass : asClasses) {
			if (asClass.getFullName().equals(fullName)) { return asClass; }
		}
		return null;
	}

	/**
	 * 
	 * @param order
	 * @param field
	 * @return
	 */
	private ASFieldAnnotation analyseFieldAnnotation(int order, Field field) {
		ASFieldAnnotation rVal = new ASFieldAnnotation();

		rVal.setOrder(order);
		rVal.setVersion(settings.getXmlAnnotationVersion());

		ASAnnotationTag tag = null;
		String xmltag = ASAnnotationTag.XMLARRAY.toString();
		if (field.getType().getName().equals(getMapping().get(xmltag))) {

			rVal.setTag(ASAnnotationTag.XMLARRAY);
			rVal.setType(getListType(field.toGenericString()));

		} else {

			for (Annotation anno : field.getAnnotations()) {
				String tagType = anno.annotationType().getSimpleName().toUpperCase();
				tag = ASAnnotationTag.valueOf(tagType);
				if (tag != null) {
					rVal.setTag(tag);
					break;
				}
			}

			if (tag == null) {
				rVal.setTag(ASAnnotationTag.XMLELEMENT);
			}
		}

		return rVal;
	}

	/**
	 * 
	 * @param genericString
	 * @return
	 */
	private String getListType(String genericString) {
		String rVal = genericString;

		// visible java.util.List<type> full-qualified-classname.field
		rVal = rVal.substring(rVal.lastIndexOf("<") + 1, rVal.lastIndexOf(">"));

		if (rVal.startsWith(settings.getBasePackage())) {
			if (!asClassExists(rVal)) {

				try {
					Class<?> clazz;
					clazz = Class.forName(rVal);
					parseThroughClazz(clazz);

				} catch (ClassNotFoundException e) {
					e.printStackTrace();
				} catch (MappingNotFoundException e) {
					e.printStackTrace();
				}

			}
		}

		return rVal;
	}

	/**
	 * 
	 * @return
	 */
	public List<ASClass> getAsClasses() {
		return asClasses;
	}

	/**
	 * 
	 * @return
	 */
	public Log getLog() {
		if (log == null) {

			synchronized (this) {
				if (log != null) {
					return log;

				}
				log = new SystemStreamLog();
			}
		}
		return log;
	}

	/**
	 * 
	 * @return
	 */
	public Properties getMapping() {
		if (mapping == null) {

			synchronized (this) {
				if (mapping != null) { return mapping; }

				try {

					InputStream in = getClass().getResourceAsStream("/resources/" + mappingFile);
					mapping = new Properties();
					mapping.load(in);
					in.close();

				} catch (IOException e) {
					getLog().error("Mapping properties not found.", e);
				}
			}
		}
		return mapping;
	}
}
